package com.googlecode.horncomb.web.spring.mvc;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.validation.BindException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.multiaction.MethodNameResolver;
import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException;
import org.springframework.web.servlet.mvc.multiaction.ParameterMethodNameResolver;
import org.springframework.web.util.NestedServletException;

/**
 * @author Daniel
 */
public class FormEventController extends BaseSimpleFormController {
	/** 默认表单事件字段名 */
	protected String requestEventFieldName = "event"; //$NON-NLS-1$
	protected String defaultEventName = ""; //$NON-NLS-1$

	private String defaultEventHandleName = "defaultEventHandle"; //$NON-NLS-1$

	/** Methods, keyed by name */
	private Map<String, Method> handlerMethodMap = new HashMap<String, Method>();
	/** Object we'll invoke methods on. Defaults to this. */
	private Object delegate;
	/**
	 * Helper object that knows how to return method names from incoming
	 * requests. Can be overridden via the methodNameResolver bean property
	 */
	private MethodNameResolver methodNameResolver;
	/** Log category to use when no mapped handler is found for a request */
	public static final String PAGE_NOT_FOUND_LOG_CATEGORY = "org.springframework.web.servlet.PageNotFound";
	/** Additional logger to use when no mapped handler is found for a request */
	protected static final Log pageNotFoundLogger = LogFactory
			.getLog(PAGE_NOT_FOUND_LOG_CATEGORY);
	/** Methods, keyed by exception class */
	private Map<Class<?>, Method> exceptionHandlerMap = new HashMap<Class<?>, Method>();

	public FormEventController() {
		super();
		this.delegate = this;
		initMethodNameResolverController();
		registerHandlerMethods(this.delegate);
	}

	public FormEventController(Object delegate) {
		super();
		this.delegate = delegate;
		initMethodNameResolverController();
		registerHandlerMethods(this.delegate);
	}

	protected void initMethodNameResolverController() {
		ParameterMethodNameResolver resolver = new ParameterMethodNameResolver();
		resolver.setParamName(this.requestEventFieldName);
		resolver.setDefaultMethodName(this.defaultEventHandleName);
		this.setMethodNameResolver(resolver);
	}

	@SuppressWarnings("unchecked")
	@Override
	protected ModelAndView onSubmit(HttpServletRequest request,
			HttpServletResponse response, Object command, BindException errors)
			throws Exception {
		try {
			Object returnValue = this.invokeMethod(request, response, command,
					errors);
			if (returnValue != null && returnValue instanceof ModelAndView) {
				return (ModelAndView) returnValue;
			} else {
				return this.getSuccessModeAndView(request, command, errors,
						((returnValue instanceof Map) ? (Map) returnValue
								: null));
			}
		} catch (NoSuchRequestHandlingMethodException ex) {
			return handleNoSuchRequestHandlingMethod(ex, request, response);
		}
	}

	public ModelAndView defaultEventHandle(HttpServletRequest request,
			HttpServletResponse response, Object command, BindException errors)
			throws Exception {
		return null;
	}

	protected ModelAndView invokeMethod(HttpServletRequest request,
			HttpServletResponse response, Object command, BindException errors)
			throws Exception {
		String methodName = this.methodNameResolver
				.getHandlerMethodName(request);
		return this.invokeNamedMethod(methodName, request, response, command,
				errors);
	}

	protected String resolveRequestEvent(HttpServletRequest request,
			Object command) {
		return request.getParameter(this.requestEventFieldName);
	}

	/**
	 * Registers all handlers methods on the delegate object.
	 */
	private void registerHandlerMethods(Object delegate) {
		this.handlerMethodMap.clear();
		this.exceptionHandlerMap.clear();

		// Look at all methods in the subclass, trying to find
		// methods that are validators according to our criteria
		Method[] methods = delegate.getClass().getMethods();
		for (int i = 0; i < methods.length; i++) {
			// We're looking for methods with given parameters.
			Method method = methods[i];
			if (isHandlerMethod(method)) {
				registerHandlerMethod(method);
			}
		}

		// Now look for exception handlers.
		for (int i = 0; i < methods.length; i++) {
			Method method = methods[i];
			if (isExceptionHandlerMethod(method)) {
				// Have an exception handler
				registerExceptionHandlerMethod(method);
			}
		}
	}

	/**
	 * Registers the supplied method as a request handler.
	 */
	private void registerHandlerMethod(Method method) {
		if (logger.isDebugEnabled()) {
			logger.debug("Found action method [" + method + "]");
		}
		this.handlerMethodMap.put(method.getName(), method);
	}

	/**
	 * Registers the supplied method as an exception handler.
	 */
	private void registerExceptionHandlerMethod(Method method) {
		this.exceptionHandlerMap.put(method.getParameterTypes()[2], method);
		if (logger.isDebugEnabled()) {
			logger.debug("Found exception handler method [" + method + "]");
		}
	}

	/**
	 * Is the supplied method a valid handler method?
	 * <p>
	 * Does not consider <code>Controller.handleRequest</code> itself as handler
	 * method (to avoid potential stack overflow).
	 */
	private boolean isHandlerMethod(Method method) {
		Class<?> returnType = method.getReturnType();
		if (ModelAndView.class.equals(returnType)
				|| Map.class.equals(returnType)
				|| void.class.equals(returnType)) {
			Class<?>[] parameterTypes = method.getParameterTypes();
			if (parameterTypes.length >= 2
					&& HttpServletRequest.class.equals(parameterTypes[0])
					&& HttpServletResponse.class.equals(parameterTypes[1])
					&& !method.getName().equals("handleRequest")) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Is the supplied method a valid exception handler method?
	 */
	private boolean isExceptionHandlerMethod(Method method) {
		return (isHandlerMethod(method)
				&& method.getParameterTypes().length == 3 && Throwable.class
				.isAssignableFrom(method.getParameterTypes()[2]));
	}

	@SuppressWarnings("unchecked")
	protected final ModelAndView invokeNamedMethod(String methodName,
			HttpServletRequest request, HttpServletResponse response,
			Object command, BindException errors) throws Exception {

		Method method = this.handlerMethodMap.get(methodName);
		// // cover to default method
		// if (method == null
		// && StringUtils.isNotEmpty(this.defaultEventHandleName)) {
		// method = this.handlerMethodMap.get(this.defaultEventHandleName);
		// }
		if (method == null) {
			throw new NoSuchRequestHandlingMethodException(methodName,
					getClass());
		}

		try {
			List params = new ArrayList(4);
			params.add(request);
			params.add(response);

			if (method.getParameterTypes().length >= 3
					&& this.getCommandClass().isAssignableFrom(
							method.getParameterTypes()[2])) {
				params.add(command);
			}

			if (method.getParameterTypes().length >= 4
					&& BindException.class.isAssignableFrom(method
							.getParameterTypes()[3])) {
				params.add(errors);
			}

			Object returnValue = method.invoke(this.delegate, params
					.toArray(new Object[params.size()]));
			if (returnValue != null && returnValue instanceof ModelAndView) {
				return (ModelAndView) returnValue;
			} else {
				return this.getSuccessModeAndView(request, command, errors,
						((returnValue instanceof Map) ? (Map) returnValue
								: null));
			}
		} catch (InvocationTargetException ex) {
			// The handler method threw an exception.
			return handleException(request, response, ex.getTargetException());
		} catch (Exception ex) {
			// The binding process threw an exception.
			return handleException(request, response, ex);
		}
	}

	private ModelAndView massageReturnValueIfNecessary(Object returnValue) {
		if (returnValue instanceof ModelAndView) {
			return (ModelAndView) returnValue;
		} else {
			// Either returned null or was 'void' return.
			// We'll assume that the handle method already wrote the response.
			return null;
		}
	}

	/**
	 * We've encountered an exception which may be recoverable
	 * (InvocationTargetException or HttpSessionRequiredException). Allow the
	 * subclass a chance to handle it.
	 * @param request
	 *            current HTTP request
	 * @param response
	 *            current HTTP response
	 * @param ex
	 *            the exception that got thrown
	 * @return a ModelAndView to render the response
	 */
	private ModelAndView handleException(HttpServletRequest request,
			HttpServletResponse response, Throwable ex) throws Exception {

		Method handler = getExceptionHandler(ex);
		if (handler != null) {
			return invokeExceptionHandler(handler, request, response, ex);
		}
		// If we get here, there was no custom handler
		if (ex instanceof Exception) {
			throw (Exception) ex;
		}
		if (ex instanceof Error) {
			throw (Error) ex;
		}
		// Should never happen!
		throw new NestedServletException("Unknown Throwable type encountered",
				ex);
	}

	/**
	 * Invoke the selected exception handler.
	 * @param handler
	 *            handler method to invoke
	 */
	private ModelAndView invokeExceptionHandler(Method handler,
			HttpServletRequest request, HttpServletResponse response,
			Throwable ex) throws Exception {

		if (handler == null) {
			throw new NestedServletException("No handler for exception", ex);
		}

		// If we get here, we have a handler.
		if (logger.isDebugEnabled()) {
			logger.debug("Invoking exception handler [" + handler
					+ "] for exception [" + ex + "]");
		}
		try {
			Object returnValue = handler.invoke(this.delegate, new Object[] {
					request, response, ex });
			return massageReturnValueIfNecessary(returnValue);
		} catch (InvocationTargetException ex2) {
			Throwable targetEx = ex2.getTargetException();
			if (targetEx instanceof Exception) {
				throw (Exception) targetEx;
			}
			if (targetEx instanceof Error) {
				throw (Error) targetEx;
			}
			// Should never happen!
			throw new NestedServletException(
					"Unknown Throwable type encountered", targetEx);
		}
	}

	/**
	 * Determine the exception handler method for the given exception. Can
	 * return null if not found.
	 * @return a handler for the given exception type, or <code>null</code>
	 * @param exception
	 *            the exception to handle
	 */
	protected Method getExceptionHandler(Throwable exception) {
		Class<?> exceptionClass = exception.getClass();
		if (logger.isDebugEnabled()) {
			logger.debug("Trying to find handler for exception class ["
					+ exceptionClass.getName() + "]");
		}
		Method handler = this.exceptionHandlerMap.get(exceptionClass);
		while (handler == null && !exceptionClass.equals(Throwable.class)) {
			if (logger.isDebugEnabled()) {
				logger
						.debug("Trying to find handler for exception superclass ["
								+ exceptionClass.getName() + "]");
			}
			exceptionClass = exceptionClass.getSuperclass();
			handler = this.exceptionHandlerMap.get(exceptionClass);
		}
		return handler;
	}

	protected ModelAndView handleNoSuchRequestHandlingMethod(
			NoSuchRequestHandlingMethodException ex,
			HttpServletRequest request, HttpServletResponse response)
			throws Exception {

		pageNotFoundLogger.warn(ex.getMessage());
		response.sendError(HttpServletResponse.SC_NOT_FOUND);
		return null;
	}

	public Map<Class<?>, Method> getExceptionHandlerMap() {
		return exceptionHandlerMap;
	}

	public void setExceptionHandlerMap(Map<Class<?>, Method> exceptionHandlerMap) {
		this.exceptionHandlerMap = exceptionHandlerMap;
	}

	public Map<String, Method> getHandlerMethodMap() {
		return handlerMethodMap;
	}

	public void setHandlerMethodMap(Map<String, Method> handlerMethodMap) {
		this.handlerMethodMap = handlerMethodMap;
	}

	public MethodNameResolver getMethodNameResolver() {
		return methodNameResolver;
	}

	public void setMethodNameResolver(MethodNameResolver methodNameResolver) {
		this.methodNameResolver = methodNameResolver;
	}

	/**
	 * @param defaultEventHandleName
	 *            the defaultEventHandleName to set
	 */
	public void setDefaultEventHandleName(String defaultMethodName) {
		this.defaultEventHandleName = defaultMethodName;
	}

	/**
	 * @param defaultEventName
	 *            the defaultEventName to set
	 */
	public void setDefaultEventName(String defaultFormEvent) {
		this.defaultEventName = defaultFormEvent;
	}

	public void setRequestEventFieldName(String eventFieldName) {
		this.requestEventFieldName = eventFieldName;
	}
}
